"""
包导入的时候最好遵循包导入顺序原则

标准库
第三方库
项目自定义
"""

import time
from contextlib import asynccontextmanager
from threading import Thread
from typing import AsyncGenerator

from anyio import CapacityLimiter
from anyio.lowlevel import RunVar
from fastapi import FastAPI, Request, Response, applications
from fastapi.exceptions import RequestValidationError
from sqlalchemy import text
from starlette.exceptions import HTTPException
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse

from app.api.routes import router as api_router
from app.core.code_msg import ServerError, InvalidParams
from app.core.config import settings
from app.core.exception import ApiException
from app.core.scheduler import scheduler
from app.db.database import get_db
from app.schemas.base import R
from app.utils.logger import logger, set_trace_id, setup_logging


def create_app() -> FastAPI:
    """
    生成FatAPI对象
    :return:
    """
    setup_logging()
    app = FastAPI(
        title=settings.PROJECT_NAME,
        description=settings.DESCRIPTION,
        version=settings.VERSION,
        lifespan=lifespan,
        # root_path=settings.CONTEXT_PATH,
        openapi_url=f"{settings.CONTEXT_PATH}/openapi.json",
        docs_url=f"{settings.CONTEXT_PATH}/docs",
        redoc_url=f"{settings.CONTEXT_PATH}/redoc",
    )
    # 跨域设置
    register_cors(app)
    # 注册路由
    register_router(app)
    # 注册捕获全局异常
    register_exception(app)
    # 请求拦截
    register_hook(app)
    # 注册静态文件
    register_static_file(app)
    return app


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    # 调整 starlette 请求数
    RunVar("_default_thread_limiter").set(CapacityLimiter(settings.MAX_THREADS_LIMIT))

    # 监听服务启动情况，打印服务启动地址
    Thread(target=print_web_url).start()

    # 应用启动
    logger.info(">>> 应用启动...")

    async for session in get_db():
        session.execute(text("select 1"))
        logger.info(">>> 数据库连接成功")

    # session = next(get_session())
    # session.execute(text("select 1"))
    # logger.info(">>> 数据库连接成功")

    try:
        logger.info(">>> 定时任务启动...")
        await scheduler.start()

        yield
    finally:
        try:
            logger.info(">>> 定时任务关闭...")
            await scheduler.shutdown()
        except Exception as e:
            logger.error(e)

    # 应用关闭
    logger.info(">>> 应用关闭...")


def register_static_file(app: FastAPI) -> None:
    """
    静态文件交互开发模式使用
    生产使用 nginx 静态资源服务
    这里是开发是方便本地
    :param app:
    :return:
    """
    import os
    from fastapi.staticfiles import StaticFiles
    if not os.path.exists("./static"):
        os.mkdir("./static")
    app.mount(settings.CONTEXT_PATH + "/static", StaticFiles(directory="static"), name="static")

    # 自定义swagger ui
    def swagger_monkey_patch(*args, **kwargs):
        from fastapi.openapi.docs import get_swagger_ui_html
        return get_swagger_ui_html(
            *args,
            **kwargs,
            swagger_js_url=f"{settings.CONTEXT_PATH}/static/swagger-ui/swagger-ui-bundle.js",
            swagger_css_url=f"{settings.CONTEXT_PATH}/static/swagger-ui/swagger-ui.css",
            swagger_favicon_url=f"{settings.CONTEXT_PATH}/static/favicon.png",
        )

    applications.get_swagger_ui_html = swagger_monkey_patch

    def redoc_monkey_patch(*args, **kwargs):
        from fastapi.openapi.docs import get_redoc_html
        return get_redoc_html(
            *args,
            **kwargs,
            redoc_js_url=f"{settings.CONTEXT_PATH}/static/redoc/redoc.standalone.js",
            redoc_favicon_url=f"{settings.CONTEXT_PATH}/static/favicon.png",
            with_google_fonts=False,
        )

    applications.get_redoc_html = redoc_monkey_patch


def register_router(app: FastAPI) -> None:
    """
    注册路由
    :param app:
    :return:
    """
    # 项目API
    app.include_router(api_router, prefix=settings.CONTEXT_PATH)

    # 重定向根路径
    @app.get("/", include_in_schema=False)
    async def redirect_root():
        return RedirectResponse(url=f"{settings.CONTEXT_PATH}/")


def register_cors(app: FastAPI) -> None:
    """
    支持跨域
    :param app:
    :return:
    """
    # app.add_middleware(
    #     CORSMiddleware,
    #     allow_origins=["*"],
    #     allow_credentials=True,
    #     allow_methods=["*"],
    #     allow_headers=["*"],
    # )
    CORSMiddleware(app, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])


def register_exception(app: FastAPI) -> None:
    """
    全局异常捕获
    注意 别手误多敲一个s
    exception_handler
    exception_handlers
    两者有区别
        如果只捕获一个异常 启动会报错
        @exception_handlers(UserNotFound)
    TypeError: 'dict' object is not callable
    :param app:
    :return:
    """

    @app.exception_handler(ApiException)
    def exception_handler(request: Request, exception: ApiException):
        """自定义异常处理"""
        logger.exception(">>> 业务异常")
        return R.fail(code=exception.code, message=exception.message, data=exception.data)

    @app.exception_handler(HTTPException)
    def exception_handler(request: Request, exception: HTTPException):
        """HTTP异常处理"""
        logger.exception(">>> HTTP异常")
        return R.fail(code_msg=ServerError.set_message(exception.detail))

    # @app.exception_handler(ValidationError)
    # async def inner_validation_exception_handler(request: Request, exc: ValidationError):
    #     """
    #     内部参数验证异常
    #     :param request:
    #     :param exc:
    #     :return:
    #     """
    #     logger.error(
    #         f"内部参数验证错误\nURL:{request.method}{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
    #     return resp.fail(resp.BusinessError.set_msg(exc.errors()))

    @app.exception_handler(RequestValidationError)
    async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
        """
        请求参数验证异常
        :param request:
        :param exc:
        :return:
        """
        # logger.error(f"请求参数格式错误\nURL:{request.method}{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
        # return response_code.resp_4001(message='; '.join([f"{e['loc'][1]}: {e['msg']}" for e in exc.errors()]))
        logger.exception(">>> 参数异常")
        return R.fail(code_msg=InvalidParams.set_message(str(exc.errors())))

    # 捕获全部异常
    @app.exception_handler(Exception)
    async def all_exception_handler(request: Request, exc: Exception):
        """
        全局所有异常
        :param request:
        :param exc:
        :return:
        """
        logger.exception(">>> 全局异常")
        # logger.error(f"全局异常\n{request.method}: {request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
        return R.fail(code_msg=ServerError.set_message(str(exc)))


def register_hook(app: FastAPI) -> None:
    """
    请求响应拦截 hook
    https://fastapi.tiangolo.com/tutorial/middleware/
    :param app:
    :return:
    """

    @app.middleware("http")
    async def logger_request(request: Request, call_next) -> Response:
        # https://stackoverflow.com/questions/60098005/fastapi-starlette-get-client-real-ip
        # logger.info(f"访问记录:{request.method} url:{request.url}\nheaders:{request.headers}\nIP:{request.client.host}")
        # response = await call_next(request)
        # return response

        """
        1.设置日志的全链路追踪
        2.记录错误日志
        """

        #  设置日志的全链路追踪
        request_id_key = "X-Request-Id"
        _req_id_val = request.headers.get(request_id_key, "")
        req_id = set_trace_id(_req_id_val)

        # 记录请求日志
        request_prefix = f"{request.client.host}:{request.client.port} - {request.method} {request.url.path}"
        content_type = request.headers.get("content-type")
        if content_type and content_type.startswith("application/json"):
            _bytes = await request.body()
            logger.info(f"{request_prefix} body ==> {_bytes.decode('utf8')}")
        elif request.query_params:
            logger.info(f"{request_prefix} query params ==> {request.query_params}")
        else:
            logger.info(f"{request_prefix}")

        # 记录请求时间
        start_time = time.perf_counter()
        response = await call_next(request)
        response.headers[request_id_key] = req_id.get()

        # 获取响应体内容
        # response_body = b""
        # async for chunk in response.body_iterator:
        #     response_body += chunk
        # logger.info(f"response body ==> {response_body.decode('utf8')}")

        # 记录响应时间
        end_time = time.perf_counter()
        process_time = (end_time - start_time) * 1000
        response.headers["X-Process-Time"] = f"{process_time:.2f}"

        logger.info(f"{request_prefix} processed in {process_time:.2f} ms - {response.status_code}")
        return response


def print_web_url():
    from urllib import request
    from app.utils.logger import logger

    # 判断地址是否启动，如果启动则打开浏览器
    url = f"http://{settings.SERVER_HOST}:{settings.SERVER_PORT}/{settings.CONTEXT_PATH.lstrip('/')}"
    retry_count = 0
    while retry_count < 120:
        retry_count += 1
        try:
            status_code = request.urlopen(f"{url}/", timeout=1).code
            if status_code in [200, 301, 307]:
                logger.info(f"服务启动成功，{"API": ^7.7}接口请访问：{url}/")
                logger.info(f"服务启动成功，Swagger文档请访问：{url}/docs")
                logger.info(f"服务启动成功，{"ReDoc": ^7.7}文档请访问：{url}/redoc")
                break
        except Exception:
            pass
